﻿//ReConnectionManager.cs
//Copyright (c) 2013 StockSharp LLC, all rights reserved.
//This code module is part of StockSharp library.
//This code is licensed under the GNU GENERAL PUBLIC LICENSE Version 3.
//See the file License.txt for the license details.
//More info on: http://stocksharp.com

namespace StockSharp.Algo
{
	using System;
	using System.Threading;

	using Ecng.Common;

	using StockSharp.BusinessEntities;
	using StockSharp.Logging;

	/// <summary>
	/// Менеджер переподсоединения <see cref="ITrader"/> к торговой системе. В случае получения ошибки соединения
	/// через <see cref="ITrader.ConnectionError"/>, менеджер проверяет соединение, и если оно отсутствует, то
	/// запускает процесс переподключения.
	/// </summary>
	class ReConnectionManager : Disposable
	{
		private enum ConnectionActions
		{
			None,
			Connecting,
			Disconnecting,
		}

		private bool _isConnected;
		private bool _isConnecting;
		private bool _isReConnecting;
		private Timer _connectingTimer;
		private Timer _reConnectingTimer;
		private DateTime _lastExportTime;
		private Timer _exportTimeOutTimer;

		private int _connectingAttempts;
		private int _reConnectingAttempts;

		private Timer _connectionTimeOutTimer;
		private ConnectionActions _connectionAction;

		private readonly Action _connectionRestored;
		private readonly Action<Exception> _attemptError;
		private readonly Action _exportTimeOut;

		private readonly object _syncLock = new object();

		public ReConnectionManager(ITrader trader, Action connectionRestored, Action<Exception> attemptError, Action exportTimeOut)
		{
			if (trader == null)
				throw new ArgumentNullException("trader");

			if (connectionRestored == null)
				throw new ArgumentNullException("connectionRestored");

			if (attemptError == null)
				throw new ArgumentNullException("attemptError");

			if (exportTimeOut == null)
				throw new ArgumentNullException("exportTimeOut");

			Settings = new ReConnectionSettings(UpdateExportTimeOut);

			Trader = trader;
			Trader.ConnectionError += OnConnectionError;
			Trader.NewDataExported += OnNewDataExported;

			_connectionRestored = connectionRestored;
			_attemptError = attemptError;
			_exportTimeOut = exportTimeOut;
		}

		public ITrader Trader { get; private set; }

		public ReConnectionSettings Settings { get; private set; }

		public void Connect(Action timeOut)
		{
			if (_connectionAction != ConnectionActions.None)
				return;

			_connectionAction = ConnectionActions.Connecting;
			DoWithTimeOut(timeOut);
		}

		public void Disconnect(Action timeOut)
		{
			switch (_connectionAction)
			{
				case ConnectionActions.Connecting:
					StopTimeOut(true);
					return;
				case ConnectionActions.Disconnecting:
					return;
			}

			_connectionAction = ConnectionActions.Disconnecting;
			DoWithTimeOut(timeOut);
		}

		private void DoWithTimeOut(Action timeOut)
		{
			if (timeOut == null)
				throw new ArgumentNullException("timeOut");

			if (Settings.ConnectDisconnectTimeOutInterval == TimeSpan.Zero)
				return;

			if (_connectionAction == ConnectionActions.Connecting)
				Trader.Connected += OnConnectDisconnectHandler;
			else
				Trader.Disconnected += OnConnectDisconnectHandler;

			Trader.ConnectionError += OnTimeOutError;

			_connectionTimeOutTimer = ThreadingHelper.Timer(() =>
			{
				OnTimeOutError(null);
				timeOut();
			})
			.Interval(Settings.ConnectDisconnectTimeOutInterval);
		}

		private void OnTimeOutError(Exception ex)
		{
			var isConnecting = _connectionAction == ConnectionActions.Connecting;
			
			StopTimeOut(false);

			if (_isConnecting)
				return;

			if (isConnecting && Settings.ConnectingAttemptCount != 0)
			{
				_isConnecting = true;
				_connectingTimer = ThreadingHelper.Timer(OnConnecting).Interval(Settings.Interval);
			}
		}

		private bool IsTradeTime()
		{
			return Settings.WorkingTime.IsTradeTime(LoggingHelper.Now);
		}

		private void OnConnecting()
		{
			lock (_syncLock)
			{
				if (!_isConnecting)
					return;

				if (!IsTradeTime())
					return;

				if (Settings.ConnectingAttemptCount > 0 && _connectingAttempts > Settings.ConnectingAttemptCount)
					DisposeConnectingTimer();

				_connectingAttempts++;

				try
				{
					if (!Trader.IsConnected)
						Trader.Connect();
					else
					{
						DisposeConnectingTimer();
					}
				}
				catch (Exception ex)
				{
					_attemptError(ex);
				}
			}
		}

		private void OnConnectDisconnectHandler()
		{
			StopTimeOut(true);
		}

		private void StopTimeOut(bool isSuccess)
		{
			_connectionTimeOutTimer.Dispose();

			Trader.ConnectionError -= OnTimeOutError;

			switch (_connectionAction)
			{
				case ConnectionActions.Connecting:
					Trader.Connected -= OnConnectDisconnectHandler;
					_isConnected = isSuccess;
					break;
				case ConnectionActions.Disconnecting:
					Trader.Disconnected -= OnConnectDisconnectHandler;
					_isConnected = false;
					break;
			}

			_connectionAction = ConnectionActions.None;
		}

		/// <summary>
		/// Обработчик события <see cref="ITrader.ConnectionError"/>.
		/// </summary>
		/// <param name="ex">Ошибка соединения.</param>
		protected virtual void OnConnectionError(Exception ex)
		{
			if (Settings.ReConnectingAttemptCount == 0 || !_isConnected || _isReConnecting)
				return;

			_reConnectingAttempts = 0;
			_isReConnecting = true;
			_reConnectingTimer = ThreadingHelper.Timer(OnReconnecting).Interval(Settings.Interval);
		}

		private void OnReconnecting()
		{
			lock (_syncLock)
			{
				if (!_isReConnecting)
					return;

				if (!IsTradeTime())
					return;

				if (Settings.ReConnectingAttemptCount > 0 && _reConnectingAttempts > Settings.ReConnectingAttemptCount)
					DisposeReConnectingTimer();

				_reConnectingAttempts++;

				try
				{
					if (!Trader.IsConnected)
						Trader.Reconnect();
					else
					{
						DisposeReConnectingTimer();

						_connectionRestored();

						if (Settings.IsReStartExport)
							Trader.ReStartExport();
					}
				}
				catch (Exception ex)
				{
					_attemptError(ex);
				}
			}
		}

		private void OnNewDataExported()
		{
			_lastExportTime = DateTime.Now;
		}

		private void UpdateExportTimeOut()
		{
			var interval = Settings.ExportTimeOutInterval;

			if (interval != TimeSpan.Zero)
			{
				if (_exportTimeOutTimer != null)
					_exportTimeOutTimer.Change(interval, interval);
				else
				{
					_exportTimeOutTimer = ThreadingHelper.Timer(() =>
					{
						// http://stocksharp.com/forum/yaf_postsm8195_riestart-eksporta-v-nierabochieie-vriemia--kak-otkliuchit.aspx
						if (!IsTradeTime())
							return;

						if (Trader.IsExportStarted && (DateTime.Now - _lastExportTime) > interval)
						{
							_exportTimeOut();
							if (Settings.IsReStartExport)
								Trader.ReStartExport();
						}
					})
					.Interval(interval);
				}
			}
			else
			{
				DisposeExportTimeOutTimer();
			}
		}

		private void DisposeExportTimeOutTimer()
		{
			if (_exportTimeOutTimer == null)
				return;

			_exportTimeOutTimer.Dispose();
			_exportTimeOutTimer = null;
		}

		private void DisposeConnectingTimer()
		{
			if (_connectingTimer == null)
				return;

			_connectingTimer.Dispose();
			_connectingTimer = null;
			_isConnecting = false;
		}

		private void DisposeReConnectingTimer()
		{
			if (_reConnectingTimer == null)
				return;

			_reConnectingTimer.Dispose();
			_reConnectingTimer = null;
			_isReConnecting = false;
		}

		/// <summary>
		/// Освободить занятые ресурсы.
		/// </summary>
		protected override void DisposeManaged()
		{
			DisposeExportTimeOutTimer();
			DisposeConnectingTimer();
			DisposeReConnectingTimer();

			Trader.NewDataExported -= OnNewDataExported;
			Trader.ConnectionError -= OnConnectionError;

			base.DisposeManaged();
		}
	}
}